# SEE modeldata package for new datasets
library(tidyverse)         # for graphing and data cleaning
library(tidymodels)        # for modeling
library(stacks)            # for stacking models
library(naniar)            # for examining missing values (NAs)
library(lubridate)         # for date manipulation
library(moderndive)        # for King County housing data
library(DALEX)             # for model interpretation  
library(DALEXtra)          # for extension of DALEX
library(patchwork)         # for combining plots nicely
library(dbplyr)            # for SQL query "cheating" - part of tidyverse but needs to be loaded separately
library(mdsr)              # for accessing some databases - goes with Modern Data Science with R textbook
library(RMySQL)            # for accessing MySQL databases
library(RSQLite)           # for accessing SQLite databases

#mapping
library(maps)              # for built-in maps
library(sf)                # for making maps using geom_sf
library(ggthemes)          # Lisa added - I like theme_map() for maps :)

#tidytext
library(tidytext)          # for text analysis, the tidy way!
library(textdata)          
library(reshape2)
library(wordcloud)         # for wordcloud
library(stopwords)

theme_set(theme_minimal()) # Lisa's favorite theme

When you finish the assignment, remove the # from the options chunk at the top, so that messages and warnings aren’t printed. If you are getting errors in your code, add error = TRUE so that the file knits. I would recommend not removing the # until you are completely finished.

Put it on GitHub!

From now on, GitHub should be part of your routine when doing assignments. I recommend making it part of your process anytime you are working in R, but I’ll make you show it’s part of your process for assignments.

Task: When you are finished with the assignment, post a link below to the GitHub repo for the assignment. If you want to post it to your personal website, that’s ok (not required). Make sure the link goes to a spot in the repo where I can easily find this assignment. For example, if you have a website with a blog and post the assignment as a blog post, link to the post’s folder in the repo. As an example, I’ve linked to my GitHub stacking material here.

Local Interpretable Machine Learning

You are going to use the King County house data and the same random forest model to predict log_price that I used in the tutorial.

Tasks:

data("house_prices")
# Create log_price and drop price variable
house_prices <- house_prices %>% 
  mutate(log_price = log(price, base = 10)) %>% 
  # make all integers numeric ... fixes prediction problem
  mutate(across(where(is.integer), as.numeric)) %>% 
  select(-price)


set.seed(327) #for reproducibility

# Randomly assigns 75% of the data to training.
house_split <- initial_split(house_prices, 
                             prop = .75)
house_training <- training(house_split)
house_testing <- testing(house_split)

#Recreate the random forest model:

# set up recipe and transformation steps and roles
ranger_recipe <- 
  recipe(formula = log_price ~ ., 
         data = house_training) %>% 
  step_date(date, 
            features = "month") %>% 
  # Make these evaluative variables, not included in modeling
  update_role(all_of(c("id",
                       "date")),
              new_role = "evaluative")

#define model
ranger_spec <- 
  rand_forest(mtry = 6, 
              min_n = 10, 
              trees = 200) %>% 
  set_mode("regression") %>% 
  set_engine("ranger")

#create workflow
ranger_workflow <- 
  workflow() %>% 
  add_recipe(ranger_recipe) %>% 
  add_model(ranger_spec) 

#fit the model
set.seed(712) # for reproducibility - random sampling in random forest choosing number of variables
ranger_fit <- ranger_workflow %>% 
  fit(house_training)
  1. Choose 3 new observations and do the following for each observation:


The three observations:
# Create an explainer for the random forest model:
rf_explain <- 
  explain_tidymodels(
    model = ranger_fit,
    data = house_training %>% select(-log_price), 
    y = house_training %>%  pull(log_price),
    label = "rf"
  )
## Preparation of a new explainer is initiated
##   -> model label       :  rf 
##   -> data              :  16210  rows  20  cols 
##   -> data              :  tibble converted into a data.frame 
##   -> target variable   :  16210  values 
##   -> predict function  :  yhat.workflow  will be used (  default  )
##   -> predicted values  :  No value for predict function target column. (  default  )
##   -> model_info        :  package tidymodels , ver. 0.1.2 , task regression (  default  ) 
##   -> predicted values  :  numerical, min =  5.057808 , mean =  5.665083 , max =  6.722883  
##   -> residual function :  difference between y and yhat (  default  )
##   -> residuals         :  numerical, min =  -0.3314416 , mean =  0.0004168269 , max =  0.2491147  
##   A new explainer has been created! 
obs1 <- house_testing %>% slice(5377) 
obs2 <- house_testing %>% slice(100) 
obs3 <- house_testing %>% slice(2498) 

obs1
obs2
obs3
# Price of new_obs's house - just to know because I can't think in logs
10^(obs1$log_price)
## [1] 1865000
# observation 1 house price is $186,5,000
10^(obs2$log_price)
## [1] 365000
# observation 2 house price is $365,000
10^(obs3$log_price)
## [1] 505000
# observation 3 house price is $505,000


Part 1


#obs1 rf
bd_rf_obs1 <- predict_parts(explainer = rf_explain,
                          new_observation = obs1,
                          type = "break_down") #default
plot(bd_rf_obs1)

bd_rf_obs1
#obs2 rf
bd_rf_obs2 <- predict_parts(explainer = rf_explain,
                          new_observation = obs2,
                          type = "break_down") #default
plot(bd_rf_obs2)

bd_rf_obs2
#obs3 rf
bd_rf_obs3 <- predict_parts(explainer = rf_explain,
                          new_observation = obs3,
                          type = "break_down") #default
plot(bd_rf_obs3)

bd_rf_obs3


Observation 1: The largest contribution is by sqft_living = 3570


Observation 2: The largest contribution is by lat = 47.6283


Observation 3: The largest contribution is by lat = 47.5722


Part 2


#obs 1
rf_shap1 <-predict_parts(explainer = rf_explain,
                        new_observation = obs1,
                        type = "shap",
                        B = 10 #number of reorderings - start small
)

plot(rf_shap1)

#obs 2
rf_shap2 <-predict_parts(explainer = rf_explain,
                        new_observation = obs2,
                        type = "shap",
                        B = 10 #number of reorderings - start small
)

plot(rf_shap2)

#obs 3
rf_shap3 <-predict_parts(explainer = rf_explain,
                        new_observation = obs3,
                        type = "shap",
                        B = 10 #number of reorderings - start small
)

plot(rf_shap3)


It does tell a story similar to the break-down plots from above.


Part 3

# NEED these two lines of code always!
# They make sure our explainer is defined correctly to use in the next step


#observation 1
set.seed(2)
model_type.dalex_explainer <- DALEXtra::model_type.dalex_explainer
predict_model.dalex_explainer <- DALEXtra::predict_model.dalex_explainer

lime_rf_1 <- predict_surrogate(explainer = rf_explain,
                             new_observation = obs1 %>%
                               select(-log_price), 
                             n_features = 7,
                             n_permutations = 1000,
                             type = "lime")
## Warning: view does not contain enough variance to use quantile binning. Using
## standard binning instead.
## Warning: yr_renovated does not contain enough variance to use quantile binning.
## Using standard binning instead.
## Warning in gower_work(x = x, y = y, pair_x = pair_x, pair_y = pair_y, n =
## NULL, : skipping variable with zero or non-finite range.
lime_rf_1 %>% 
  select(model_r2, model_prediction, prediction) %>% 
  distinct()
plot(lime_rf_1) +
  labs(x = "Variable")
## Warning in lime::plot_features(x, ...): NAs introduced by coercion
## Warning: Removed 1 rows containing missing values (position_stack).

#observation 2
set.seed(2)
model_type.dalex_explainer <- DALEXtra::model_type.dalex_explainer
predict_model.dalex_explainer <- DALEXtra::predict_model.dalex_explainer

lime_rf_2 <- predict_surrogate(explainer = rf_explain,
                             new_observation = obs2 %>%
                               select(-log_price), 
                             n_features = 7,
                             n_permutations = 1000,
                             type = "lime")
## Warning: view does not contain enough variance to use quantile binning. Using
## standard binning instead.
## Warning: yr_renovated does not contain enough variance to use quantile binning.
## Using standard binning instead.
## Warning in gower_work(x = x, y = y, pair_x = pair_x, pair_y = pair_y, n =
## NULL, : skipping variable with zero or non-finite range.
lime_rf_2 %>% 
  select(model_r2, model_prediction, prediction) %>% 
  distinct()
plot(lime_rf_2) +
  labs(x = "Variable")

#observation 3
set.seed(2)
model_type.dalex_explainer <- DALEXtra::model_type.dalex_explainer
predict_model.dalex_explainer <- DALEXtra::predict_model.dalex_explainer

lime_rf_3 <- predict_surrogate(explainer = rf_explain,
                             new_observation = obs3 %>%
                               select(-log_price), 
                             n_features = 7,
                             n_permutations = 1000,
                             type = "lime")
## Warning: view does not contain enough variance to use quantile binning. Using
## standard binning instead.
## Warning: yr_renovated does not contain enough variance to use quantile binning.
## Using standard binning instead.
## Warning in gower_work(x = x, y = y, pair_x = pair_x, pair_y = pair_y, n =
## NULL, : skipping variable with zero or non-finite range.
lime_rf_3 %>% 
  select(model_r2, model_prediction, prediction) %>% 
  distinct()
plot(lime_rf_3) +
  labs(x = "Variable")


The bars show which variables are most important. The Explanation fit is an overall performance metric for the local model - model_r2 from the output above. Observation 1 has the highest explanation fit (0.37), so it is the best model. It means that it’s prediction from this local model is closest to its prediction from the original model.


Part 3

I think the tools we’ve learned would be very helpful for predictive models, especially when there may be lots of variables and it’ll help us narrow down the best variables to make the best predictions.

SQL

You will use the airlines data from the SQL database that I used in the example in the tutorial. Be sure to include the chunk to connect to the database here. And, when you are finished, disconnect. You may need to reconnect throughout as it times out after a while.

Tasks:

Part 1

con_air <- dbConnect(RMySQL::MySQL(), 
                     dbname = "airlines", 
                     host = "mdsr.cdc7tgkkqd0n.us-east-1.rds.amazonaws.com", 
                     user = "mdsr_public", 
                     password = "ImhsmflMDSwR")

dbListTables(con_air)
## [1] "airports" "carriers" "flights"  "planes"
#lapply(dbListConnections(MySQL()), dbDisconnect)


flights <-
  tbl(con_air, "flights") %>%
  select(month, arr_delay, origin, dest, air_time, distance) %>%
  head(100)

#by airport
flights_air <- 
  flights %>%
  group_by(origin) %>%
  summarize(n_flights = n(),
  avg_length = mean(air_time),
  avg_distance = mean(distance)) %>% 
  inner_join(tbl(con_air, "airports"), 
              by = c("origin" = "faa"))
## Warning in .local(conn, statement, ...): Decimal MySQL column 2 imported as
## numeric
## Warning in .local(conn, statement, ...): Decimal MySQL column 3 imported as
## numeric
air <- 
  tbl(con_air, "flights") %>% 
  head(100) %>% 
  group_by(origin) %>% 
  summarize(prop_late_over20 = mean(arr_delay > 20)) %>% 
  arrange(desc(prop_late_over20)) %>%
  rename(
     prop_late_airport = prop_late_over20) #%>%
   #as.data.frame(air, row.names = TRUE)

airport_info <-
  flights_air %>%
  inner_join(air,
            by = c("origin" = "origin")) %>%
  select(name, n_flights, avg_distance, avg_length, prop_late_airport) %>%
  arrange(desc(prop_late_airport))
  #as.data.frame(flights_air, row.names = TRUE)

#by month
flights_mon <- 
  flights %>%
  group_by(month) %>%
  summarize(n_flights = n(),
  avg_length = mean(air_time),
  avg_distance = mean(distance)) 

mon <- 
  tbl(con_air, "flights") %>% 
  head(100) %>% 
  group_by(month) %>% 
  summarize(prop_late_over20 = mean(arr_delay > 20)) %>% 
  arrange(desc(prop_late_over20)) %>%
  #as.data.frame(mon, row.names = TRUE) %>%
  rename(
    prop_late_month = prop_late_over20)

month_info <-
flights_mon %>%
  inner_join(mon,
            by = c("month" = "month")) %>%
  select(month, n_flights, avg_distance, avg_length, prop_late_month) %>%
  arrange(desc(prop_late_month))
  #as.data.frame(flights_mon, row.names = TRUE)
SQL portion
con_air <- dbConnect(RMySQL::MySQL(), 
                     dbname = "airlines", 
                     host = "mdsr.cdc7tgkkqd0n.us-east-1.rds.amazonaws.com", 
                     user = "mdsr_public", 
                     password = "ImhsmflMDSwR")

dbListTables(con_air)
## [1] "airports" "carriers" "flights"  "planes"
airport_info %>%
  show_query()
## <SQL>
## Warning: Missing values are always removed in SQL.
## Use `mean(x, na.rm = TRUE)` to silence this warning
## This warning is displayed only once per session.
## SELECT `name`, `n_flights`, `avg_distance`, `avg_length`, `prop_late_airport`
## FROM (SELECT `LHS`.`origin` AS `origin`, `LHS`.`n_flights` AS `n_flights`, `LHS`.`avg_length` AS `avg_length`, `LHS`.`avg_distance` AS `avg_distance`, `LHS`.`name` AS `name`, `LHS`.`lat` AS `lat`, `LHS`.`lon` AS `lon`, `LHS`.`alt` AS `alt`, `LHS`.`tz` AS `tz`, `LHS`.`dst` AS `dst`, `LHS`.`city` AS `city`, `LHS`.`country` AS `country`, `RHS`.`prop_late_airport` AS `prop_late_airport`
## FROM (SELECT `LHS`.`origin` AS `origin`, `LHS`.`n_flights` AS `n_flights`, `LHS`.`avg_length` AS `avg_length`, `LHS`.`avg_distance` AS `avg_distance`, `RHS`.`name` AS `name`, `RHS`.`lat` AS `lat`, `RHS`.`lon` AS `lon`, `RHS`.`alt` AS `alt`, `RHS`.`tz` AS `tz`, `RHS`.`dst` AS `dst`, `RHS`.`city` AS `city`, `RHS`.`country` AS `country`
## FROM (SELECT `origin`, COUNT(*) AS `n_flights`, AVG(`air_time`) AS `avg_length`, AVG(`distance`) AS `avg_distance`
## FROM (SELECT *
## FROM (SELECT `month`, `arr_delay`, `origin`, `dest`, `air_time`, `distance`
## FROM `flights`) `dbplyr_001`
## LIMIT 100) `dbplyr_002`
## GROUP BY `origin`) `LHS`
## INNER JOIN `airports` AS `RHS`
## ON (`LHS`.`origin` = `RHS`.`faa`)
## ) `LHS`
## INNER JOIN (SELECT `origin`, `prop_late_over20` AS `prop_late_airport`
## FROM (SELECT *
## FROM (SELECT `origin`, AVG(`arr_delay` > 20.0) AS `prop_late_over20`
## FROM (SELECT *
## FROM `flights`
## LIMIT 100) `dbplyr_003`
## GROUP BY `origin`) `dbplyr_004`
## ORDER BY `prop_late_over20` DESC) `dbplyr_005`) `RHS`
## ON (`LHS`.`origin` = `RHS`.`origin`)
## ) `dbplyr_006`
## ORDER BY `prop_late_airport` DESC
month_info %>%
  show_query()
## <SQL>
## SELECT `month`, `n_flights`, `avg_distance`, `avg_length`, `prop_late_month`
## FROM (SELECT `LHS`.`month` AS `month`, `LHS`.`n_flights` AS `n_flights`, `LHS`.`avg_length` AS `avg_length`, `LHS`.`avg_distance` AS `avg_distance`, `RHS`.`prop_late_month` AS `prop_late_month`
## FROM (SELECT `month`, COUNT(*) AS `n_flights`, AVG(`air_time`) AS `avg_length`, AVG(`distance`) AS `avg_distance`
## FROM (SELECT *
## FROM (SELECT `month`, `arr_delay`, `origin`, `dest`, `air_time`, `distance`
## FROM `flights`) `dbplyr_007`
## LIMIT 100) `dbplyr_008`
## GROUP BY `month`) `LHS`
## INNER JOIN (SELECT `month`, `prop_late_over20` AS `prop_late_month`
## FROM (SELECT *
## FROM (SELECT `month`, AVG(`arr_delay` > 20.0) AS `prop_late_over20`
## FROM (SELECT *
## FROM `flights`
## LIMIT 100) `dbplyr_009`
## GROUP BY `month`) `dbplyr_010`
## ORDER BY `prop_late_over20` DESC) `dbplyr_011`) `RHS`
## ON (`LHS`.`month` = `RHS`.`month`)
## ) `dbplyr_012`
## ORDER BY `prop_late_month` DESC


airport information
SELECT `name`, `n_flights`, `avg_distance`, `avg_length`, `prop_late_airport`
FROM (SELECT `LHS`.`origin` AS `origin`, `LHS`.`n_flights` AS `n_flights`, `LHS`.`avg_length` AS `avg_length`, `LHS`.`avg_distance` AS `avg_distance`, `LHS`.`name` AS `name`, `LHS`.`lat` AS `lat`, `LHS`.`lon` AS `lon`, `LHS`.`alt` AS `alt`, `LHS`.`tz` AS `tz`, `LHS`.`dst` AS `dst`, `LHS`.`city` AS `city`, `LHS`.`country` AS `country`, `RHS`.`prop_late_airport` AS `prop_late_airport`
FROM (SELECT `LHS`.`origin` AS `origin`, `LHS`.`n_flights` AS `n_flights`, `LHS`.`avg_length` AS `avg_length`, `LHS`.`avg_distance` AS `avg_distance`, `RHS`.`name` AS `name`, `RHS`.`lat` AS `lat`, `RHS`.`lon` AS `lon`, `RHS`.`alt` AS `alt`, `RHS`.`tz` AS `tz`, `RHS`.`dst` AS `dst`, `RHS`.`city` AS `city`, `RHS`.`country` AS `country`
FROM (SELECT `origin`, COUNT(*) AS `n_flights`, AVG(`air_time`) AS `avg_length`, AVG(`distance`) AS `avg_distance`
FROM (SELECT *
FROM (SELECT `month`, `arr_delay`, `origin`, `dest`, `air_time`, `distance`
FROM `flights`) `dbplyr_290`
LIMIT 100) `dbplyr_291`
GROUP BY `origin`) `LHS`
INNER JOIN `airports` AS `RHS`
ON (`LHS`.`origin` = `RHS`.`faa`)
) `LHS`
INNER JOIN (SELECT `origin`, `prop_late_over20` AS `prop_late_airport`
FROM (SELECT *
FROM (SELECT `origin`, AVG(`arr_delay` > 20.0) AS `prop_late_over20`
FROM (SELECT *
FROM `flights`
LIMIT 100) `dbplyr_292`
GROUP BY `origin`) `dbplyr_293`
ORDER BY `prop_late_over20` DESC) `dbplyr_294`) `RHS`
ON (`LHS`.`origin` = `RHS`.`origin`)
) `dbplyr_295`
ORDER BY `prop_late_airport` DESC
Displaying records 1 - 10
name n_flights avg_distance avg_length prop_late_airport
Hartsfield Jackson Atlanta Intl 2 569.5000 78.5000 1.0000
Fort Lauderdale Hollywood Intl 1 1119.0000 131.0000 1.0000
Southwest Florida Intl 1 1102.0000 129.0000 1.0000
La Guardia 2 1013.0000 141.5000 1.0000
Metropolitan Oakland Intl 1 2409.0000 280.0000 1.0000
Norman Y Mineta San Jose Intl 1 2570.0000 293.0000 1.0000
Palm Beach Intl 1 1028.0000 120.0000 1.0000
Washington Dulles Intl 1 542.0000 82.0000 1.0000
Newark Liberty Intl 3 607.3333 93.6667 0.6667
John F Kennedy Intl 4 1266.0000 160.0000 0.5000


monthly information
SELECT `month`, `n_flights`, `avg_distance`, `avg_length`, `prop_late_month`
FROM (SELECT `LHS`.`month` AS `month`, `LHS`.`n_flights` AS `n_flights`, `LHS`.`avg_length` AS `avg_length`, `LHS`.`avg_distance` AS `avg_distance`, `RHS`.`prop_late_month` AS `prop_late_month`
FROM (SELECT `month`, COUNT(*) AS `n_flights`, AVG(`air_time`) AS `avg_length`, AVG(`distance`) AS `avg_distance`
FROM (SELECT *
FROM (SELECT `month`, `arr_delay`, `origin`, `dest`, `air_time`, `distance`
FROM `flights`) `dbplyr_296`
LIMIT 100) `dbplyr_297`
GROUP BY `month`) `LHS`
INNER JOIN (SELECT `month`, `prop_late_over20` AS `prop_late_month`
FROM (SELECT *
FROM (SELECT `month`, AVG(`arr_delay` > 20.0) AS `prop_late_over20`
FROM (SELECT *
FROM `flights`
LIMIT 100) `dbplyr_298`
GROUP BY `month`) `dbplyr_299`
ORDER BY `prop_late_over20` DESC) `dbplyr_300`) `RHS`
ON (`LHS`.`month` = `RHS`.`month`)
) `dbplyr_301`
ORDER BY `prop_late_month` DESC
1 records
month n_flights avg_distance avg_length prop_late_month
10 100 972.72 122.43 0.23
airport_df <- as.data.frame(airport_info, row.names = TRUE)
## Warning in .local(conn, statement, ...): Decimal MySQL column 2 imported as
## numeric
## Warning in .local(conn, statement, ...): Decimal MySQL column 3 imported as
## numeric
## Warning in .local(conn, statement, ...): Decimal MySQL column 4 imported as
## numeric
month_df <- as.data.frame(month_info, row.names = TRUE)
## Warning in .local(conn, statement, ...): Decimal MySQL column 2 imported as
## numeric
## Warning in .local(conn, statement, ...): Decimal MySQL column 3 imported as
## numeric
## Warning in .local(conn, statement, ...): Decimal MySQL column 4 imported as
## numeric
worst_10_airports <-
  airport_df %>%
  select(name, n_flights, avg_distance, avg_length, prop_late_airport) %>%
  arrange(desc(prop_late_airport)) %>%
  head(10)


ggplot(worst_10_airports, aes(x = name, y = prop_late_airport)) + 
  geom_col() +
  theme(axis.text.x = element_text(angle = 60, hjust = 1)) +
  labs(title = "10 Worst Airports", x = "Airport", y = "Proportion of >20 mins late")

knitr::kable(head(airport_df[1:6, c(1,5)]), "simple")
name prop_late_airport
Washington Dulles Intl 1
Hartsfield Jackson Atlanta Intl 1
Fort Lauderdale Hollywood Intl 1
Southwest Florida Intl 1
La Guardia 1
Metropolitan Oakland Intl 1

Part 2

Function Friday

If you need to revisit the material, it is posted on the moodle page. I’ve tried to add all the necessary libraries to the top, but I may have missed something.

geom_sf() tasks:

Part 1

library("maps")
library("lwgeom")
## Linking to liblwgeom 3.0.0beta1 r16016, GEOS 3.8.1, PROJ 6.3.1
states <- st_as_sf(map("state", plot = FALSE, fill = TRUE))
head(states)
states <- states %>%
  mutate(area = as.numeric(st_area(states)))


ggplot(data = states) +
    geom_sf(aes(fill = area)) +
    scale_fill_viridis_c(trans = "sqrt", alpha = .4) +
    coord_sf(xlim = c(-127, -63), 
ylim = c(24, 51), 
expand = FALSE)


Part 2

states <- cbind(states, st_coordinates(st_centroid(states)))
## Warning in st_centroid.sf(states): st_centroid assumes attributes are constant
## over geometries of x
## Warning in st_centroid.sfc(st_geometry(x), of_largest_polygon =
## of_largest_polygon): st_centroid does not give correct centroids for longitude/
## latitude data
ggplot(data = states) +
    geom_sf(aes(fill = area)) +
    scale_fill_viridis_c(trans = "sqrt", alpha = .4) +
    geom_point(data = states, aes(X, Y), size = 1) +
    coord_sf(xlim = c(-127, -63), ylim = c(24, 51), 
expand = FALSE)


Part 3

counties <- st_as_sf(map("county", plot = FALSE, fill = TRUE))
counties <- subset(counties)
counties$area <- as.numeric(st_area(counties))
head(counties)
ggplot(data = states) +
    geom_sf(aes(fill = area)) +
    geom_sf(data = counties, fill = NA, color = gray(.5)) +
    scale_fill_viridis_c(trans = "sqrt", alpha = .4) +
    geom_point(data = states, aes(X, Y), size = 1) +
    coord_sf(xlim = c(-127, -63), ylim = c(24, 51), 
expand = FALSE)


Part 4

ggplot(data = states) +
    geom_sf(aes(fill = area)) +
    geom_sf(data = counties, fill = NA, color = gray(.5)) +
    scale_fill_viridis_c(trans = "sqrt", alpha = .4) +
    geom_text(data = states, aes(X, Y, label = ID), size = 4) +
    coord_sf(xlim = c(-125, -114), ylim = c(30, 42), expand = FALSE)


tidytext tasks:

Now you will try using tidytext on a new dataset about Russian Troll tweets.

Read about the data

These are tweets from Twitter handles that are connected to the Internet Research Agency (IRA), a Russian “troll factory.” The majority of these tweets were posted from 2015-2017, but the datasets encompass tweets from February 2012 to May 2018.

Three of the main categories of troll tweet that we will be focusing on are Left Trolls, Right Trolls, and News Feed. Left Trolls usually pretend to be BLM activists, aiming to divide the democratic party (in this context, being pro-Bernie so that votes are taken away from Hillary). Right trolls imitate Trump supporters, and News Feed handles are “local news aggregators,” typically linking to legitimate news.

For our upcoming analyses, some important variables are:

  • author (handle sending the tweet)
  • content (text of the tweet)
  • language (language of the tweet)
  • publish_date (date and time the tweet was sent)

Variable documentation can be found on Github and a more detailed description of the dataset can be found in this fivethirtyeight article.

Because there are 12 datasets containing 2,973,371 tweets sent by 2,848 Twitter handles in total, we will be using three of these datasets (one from a Right troll, one from a Left troll, and one from a News Feed account).



Part 1
troll_tweets <- read_csv("https://raw.githubusercontent.com/fivethirtyeight/russian-troll-tweets/master/IRAhandle_tweets_12.csv")
## Parsed with column specification:
## cols(
##   .default = col_character(),
##   external_author_id = col_double(),
##   following = col_double(),
##   followers = col_double(),
##   updates = col_double(),
##   post_type = col_logical(),
##   retweet = col_double(),
##   new_june_2018 = col_double(),
##   alt_external_id = col_double(),
##   tweet_id = col_double(),
##   tco3_step1 = col_logical()
## )
## See spec(...) for full column specifications.
## Warning: 110978 parsing failures.
##   row       col           expected  actual                                                                                                    file
## 14784 post_type 1/0/T/F/TRUE/FALSE RETWEET 'https://raw.githubusercontent.com/fivethirtyeight/russian-troll-tweets/master/IRAhandle_tweets_12.csv'
## 26336 post_type 1/0/T/F/TRUE/FALSE RETWEET 'https://raw.githubusercontent.com/fivethirtyeight/russian-troll-tweets/master/IRAhandle_tweets_12.csv'
## 27167 post_type 1/0/T/F/TRUE/FALSE RETWEET 'https://raw.githubusercontent.com/fivethirtyeight/russian-troll-tweets/master/IRAhandle_tweets_12.csv'
## 27168 post_type 1/0/T/F/TRUE/FALSE RETWEET 'https://raw.githubusercontent.com/fivethirtyeight/russian-troll-tweets/master/IRAhandle_tweets_12.csv'
## 27169 post_type 1/0/T/F/TRUE/FALSE RETWEET 'https://raw.githubusercontent.com/fivethirtyeight/russian-troll-tweets/master/IRAhandle_tweets_12.csv'
## ..... ......... .................. ....... .......................................................................................................
## See problems(...) for more details.


Part 2
troll_tweets
troll_tweets <- 
  troll_tweets %>%
  filter(language == "English") 

dim(troll_tweets)
## [1] 175966     21
#Dimensions are 175966 by 21


library(ggplot2)

ggplot(troll_tweets, aes(x = region)) + 
  geom_bar()

ggplot(troll_tweets, aes(x = region, fill=account_category)) + 
  geom_bar()+
  theme(axis.text.x = element_text(angle = 60, hjust = 1)) +

ggplot(troll_tweets, aes(x = region, fill=account_type)) + 
  geom_bar()+
  theme(axis.text.x = element_text(angle = 60, hjust = 1)) 

Part 3
troll_tweets_untoken <- troll_tweets %>%
  unnest_tokens(word,content)

troll_tweets_untoken



Part 4
#get rid of stopwords (the, and, etc.)

troll_tweets_cleaned <- troll_tweets_untoken %>%
  anti_join(get_stopwords())
## Joining, by = "word"
troll_tweets_cleaned <- troll_tweets_cleaned %>%
  filter(word != "http") %>% 
  filter(word != "https") %>%
  filter(word != "t.co") %>% 
  filter(word != "rt") %>%
  filter(word != "amp") %>%
  filter(word != "t,co") %>%
  filter(word != "amp") %>%
  filter(word != (1:9)) 

  
troll_tweets_cleaned


Part 5
troll_tweets_small <- troll_tweets_cleaned %>%
  count(word) %>%
  slice_max(order_by = n, n = 50) # 50 most occurring words

troll_tweets_small
# visualize the number of times the 50 top words appear
ggplot(troll_tweets_small, 
       aes(x = word, y = n)) +
  geom_col() +
  theme(axis.text.x = element_text(angle = 60, hjust = 1)) 


Part 6
# look at sentiment
sentiment <- get_sentiments("bing")
sentiment
# assign a sentiment to each word that has one associated
troll_tweets_sentiment <- troll_tweets_cleaned %>%
  inner_join(sentiment,
             by = c("word" = "word"))

troll_tweets_sentiment
# count the sentiments
troll_tweets_sentiment %>% 
  group_by(sentiment) %>%
  count()


There are more negative words than positive words. I think this is because these are troll tweets they are more likely to be negative.


  1. Using the troll_tweets_small dataset, make a wordcloud:
  1. That is sized by the number of times that a word appears in the tweets
  2. That is colored by sentiment (positive or negative)

Be sure to remove the eval=FALSE!!!!

# make a wordcloud where the size of the word is based on the number of times the word appears across the tweets

troll_tweets_small %>%
  with(wordcloud(word, n, max.words = 35))

troll_tweets_sentiment
# make a wordcloud colored by sentiment
troll_tweets_sentiment %>%
  acast(word ~ sentiment) %>%
  comparison.cloud(colors = c("red","blue"),
                   max.words = 35)
## Using sentiment as value column: use value.var to override.
## Aggregation function missing: defaulting to length


No, they’re not really surprising.

Projects


I am currently interested in looking at rental and housing data and think it would be cool to do a predictive model for housing, looking at neighborhoods, school districts, parks, crime etc. I will say it feels like our group didn’t really come to a conclusion on what we want to work with, but I would be interested in doing apartment rentals and housing market. Also, the shooting dataset also looks very interesting.

“Undoing” bias

Task:


For me, the most important take away from the thread was that bias can emerge during any part of the (Machine learning process???). This includes some of the more obvious sources like data procurement where the sample may be biased. But it can also include a seemingly bias free algorithm. For example, the introduction of a machine learning algorithm to judgement decisions for criminal defendants actually led to an increase in racial disparities. This is because judges overrode the algorithms scores more in predominately black communities compared to predominately white communities. Additionally, judges were more likely to overturn the algorithms decision for a harsher sentence if the sentence was black.


LS0tCnRpdGxlOiAnQXNzaWdubWVudCAjMycKb3V0cHV0OiAKICBodG1sX2RvY3VtZW50OgogICAgdG9jOiB0cnVlCiAgICB0b2NfZmxvYXQ6IHRydWUKICAgIGRmX3ByaW50OiBwYWdlZAogICAgY29kZV9kb3dubG9hZDogdHJ1ZQotLS0KCmBgYHtyIHNldHVwLCBpbmNsdWRlPUZBTFNFfQoja25pdHI6Om9wdHNfY2h1bmskc2V0KGVjaG8gPSBUUlVFLCBtZXNzYWdlPUZBTFNFLCB3YXJuaW5nPUZBTFNFKQpgYGAKCmBgYHtyIGxpYnJhcmllcywgbWVzc2FnZT1GQUxTRX0KIyBTRUUgbW9kZWxkYXRhIHBhY2thZ2UgZm9yIG5ldyBkYXRhc2V0cwpsaWJyYXJ5KHRpZHl2ZXJzZSkgICAgICAgICAjIGZvciBncmFwaGluZyBhbmQgZGF0YSBjbGVhbmluZwpsaWJyYXJ5KHRpZHltb2RlbHMpICAgICAgICAjIGZvciBtb2RlbGluZwpsaWJyYXJ5KHN0YWNrcykgICAgICAgICAgICAjIGZvciBzdGFja2luZyBtb2RlbHMKbGlicmFyeShuYW5pYXIpICAgICAgICAgICAgIyBmb3IgZXhhbWluaW5nIG1pc3NpbmcgdmFsdWVzIChOQXMpCmxpYnJhcnkobHVicmlkYXRlKSAgICAgICAgICMgZm9yIGRhdGUgbWFuaXB1bGF0aW9uCmxpYnJhcnkobW9kZXJuZGl2ZSkgICAgICAgICMgZm9yIEtpbmcgQ291bnR5IGhvdXNpbmcgZGF0YQpsaWJyYXJ5KERBTEVYKSAgICAgICAgICAgICAjIGZvciBtb2RlbCBpbnRlcnByZXRhdGlvbiAgCmxpYnJhcnkoREFMRVh0cmEpICAgICAgICAgICMgZm9yIGV4dGVuc2lvbiBvZiBEQUxFWApsaWJyYXJ5KHBhdGNod29yaykgICAgICAgICAjIGZvciBjb21iaW5pbmcgcGxvdHMgbmljZWx5CmxpYnJhcnkoZGJwbHlyKSAgICAgICAgICAgICMgZm9yIFNRTCBxdWVyeSAiY2hlYXRpbmciIC0gcGFydCBvZiB0aWR5dmVyc2UgYnV0IG5lZWRzIHRvIGJlIGxvYWRlZCBzZXBhcmF0ZWx5CmxpYnJhcnkobWRzcikgICAgICAgICAgICAgICMgZm9yIGFjY2Vzc2luZyBzb21lIGRhdGFiYXNlcyAtIGdvZXMgd2l0aCBNb2Rlcm4gRGF0YSBTY2llbmNlIHdpdGggUiB0ZXh0Ym9vawpsaWJyYXJ5KFJNeVNRTCkgICAgICAgICAgICAjIGZvciBhY2Nlc3NpbmcgTXlTUUwgZGF0YWJhc2VzCmxpYnJhcnkoUlNRTGl0ZSkgICAgICAgICAgICMgZm9yIGFjY2Vzc2luZyBTUUxpdGUgZGF0YWJhc2VzCgojbWFwcGluZwpsaWJyYXJ5KG1hcHMpICAgICAgICAgICAgICAjIGZvciBidWlsdC1pbiBtYXBzCmxpYnJhcnkoc2YpICAgICAgICAgICAgICAgICMgZm9yIG1ha2luZyBtYXBzIHVzaW5nIGdlb21fc2YKbGlicmFyeShnZ3RoZW1lcykgICAgICAgICAgIyBMaXNhIGFkZGVkIC0gSSBsaWtlIHRoZW1lX21hcCgpIGZvciBtYXBzIDopCgojdGlkeXRleHQKbGlicmFyeSh0aWR5dGV4dCkgICAgICAgICAgIyBmb3IgdGV4dCBhbmFseXNpcywgdGhlIHRpZHkgd2F5IQpsaWJyYXJ5KHRleHRkYXRhKSAgICAgICAgICAKbGlicmFyeShyZXNoYXBlMikKbGlicmFyeSh3b3JkY2xvdWQpICAgICAgICAgIyBmb3Igd29yZGNsb3VkCmxpYnJhcnkoc3RvcHdvcmRzKQoKdGhlbWVfc2V0KHRoZW1lX21pbmltYWwoKSkgIyBMaXNhJ3MgZmF2b3JpdGUgdGhlbWUKYGBgCgpXaGVuIHlvdSBmaW5pc2ggdGhlIGFzc2lnbm1lbnQsIHJlbW92ZSB0aGUgYCNgIGZyb20gdGhlIG9wdGlvbnMgY2h1bmsgYXQgdGhlIHRvcCwgc28gdGhhdCBtZXNzYWdlcyBhbmQgd2FybmluZ3MgYXJlbid0IHByaW50ZWQuIElmIHlvdSBhcmUgZ2V0dGluZyBlcnJvcnMgaW4geW91ciBjb2RlLCBhZGQgYGVycm9yID0gVFJVRWAgc28gdGhhdCB0aGUgZmlsZSBrbml0cy4gSSB3b3VsZCByZWNvbW1lbmQgbm90IHJlbW92aW5nIHRoZSBgI2AgdW50aWwgeW91IGFyZSBjb21wbGV0ZWx5IGZpbmlzaGVkLgoKIyMgUHV0IGl0IG9uIEdpdEh1YiEgICAgICAgIAoKRnJvbSBub3cgb24sIEdpdEh1YiBzaG91bGQgYmUgcGFydCBvZiB5b3VyIHJvdXRpbmUgd2hlbiBkb2luZyBhc3NpZ25tZW50cy4gSSByZWNvbW1lbmQgbWFraW5nIGl0IHBhcnQgb2YgeW91ciBwcm9jZXNzIGFueXRpbWUgeW91IGFyZSB3b3JraW5nIGluIFIsIGJ1dCBJJ2xsIG1ha2UgeW91IHNob3cgaXQncyBwYXJ0IG9mIHlvdXIgcHJvY2VzcyBmb3IgYXNzaWdubWVudHMuCgoqKlRhc2sqKjogV2hlbiB5b3UgYXJlIGZpbmlzaGVkIHdpdGggdGhlIGFzc2lnbm1lbnQsIHBvc3QgYSBsaW5rIGJlbG93IHRvIHRoZSBHaXRIdWIgcmVwbyBmb3IgdGhlIGFzc2lnbm1lbnQuIElmIHlvdSB3YW50IHRvIHBvc3QgaXQgdG8geW91ciBwZXJzb25hbCB3ZWJzaXRlLCB0aGF0J3Mgb2sgKG5vdCByZXF1aXJlZCkuIE1ha2Ugc3VyZSB0aGUgbGluayBnb2VzIHRvIGEgc3BvdCBpbiB0aGUgcmVwbyB3aGVyZSBJIGNhbiBlYXNpbHkgZmluZCB0aGlzIGFzc2lnbm1lbnQuIEZvciBleGFtcGxlLCBpZiB5b3UgaGF2ZSBhIHdlYnNpdGUgd2l0aCBhIGJsb2cgYW5kIHBvc3QgdGhlIGFzc2lnbm1lbnQgYXMgYSBibG9nIHBvc3QsIGxpbmsgdG8gdGhlIHBvc3QncyBmb2xkZXIgaW4gdGhlIHJlcG8uIEFzIGFuIGV4YW1wbGUsIEkndmUgbGlua2VkIHRvIG15IEdpdEh1YiBzdGFja2luZyBtYXRlcmlhbCBbaGVyZV0oaHR0cHM6Ly9naXRodWIuY29tL2xsZW5kd2F5L2Fkc193ZWJzaXRlL3RyZWUvbWFzdGVyL19wb3N0cy8yMDIxLTAzLTIyLXN0YWNraW5nKS4KCgoKIyMgTG9jYWwgSW50ZXJwcmV0YWJsZSBNYWNoaW5lIExlYXJuaW5nCgpZb3UgYXJlIGdvaW5nIHRvIHVzZSB0aGUgS2luZyBDb3VudHkgaG91c2UgZGF0YSBhbmQgdGhlIHNhbWUgcmFuZG9tIGZvcmVzdCBtb2RlbCB0byBwcmVkaWN0IGBsb2dfcHJpY2VgIHRoYXQgSSB1c2VkIGluIHRoZSBbdHV0b3JpYWxdKGh0dHBzOi8vYWR2YW5jZWQtZHMtaW4tci5uZXRsaWZ5LmFwcC9wb3N0cy8yMDIxLTAzLTMxLWltbGxvY2FsLykuCgoqKlRhc2tzOioqCgpgYGB7cn0KZGF0YSgiaG91c2VfcHJpY2VzIikKIyBDcmVhdGUgbG9nX3ByaWNlIGFuZCBkcm9wIHByaWNlIHZhcmlhYmxlCmhvdXNlX3ByaWNlcyA8LSBob3VzZV9wcmljZXMgJT4lIAogIG11dGF0ZShsb2dfcHJpY2UgPSBsb2cocHJpY2UsIGJhc2UgPSAxMCkpICU+JSAKICAjIG1ha2UgYWxsIGludGVnZXJzIG51bWVyaWMgLi4uIGZpeGVzIHByZWRpY3Rpb24gcHJvYmxlbQogIG11dGF0ZShhY3Jvc3Mod2hlcmUoaXMuaW50ZWdlciksIGFzLm51bWVyaWMpKSAlPiUgCiAgc2VsZWN0KC1wcmljZSkKCgpzZXQuc2VlZCgzMjcpICNmb3IgcmVwcm9kdWNpYmlsaXR5CgojIFJhbmRvbWx5IGFzc2lnbnMgNzUlIG9mIHRoZSBkYXRhIHRvIHRyYWluaW5nLgpob3VzZV9zcGxpdCA8LSBpbml0aWFsX3NwbGl0KGhvdXNlX3ByaWNlcywgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgcHJvcCA9IC43NSkKaG91c2VfdHJhaW5pbmcgPC0gdHJhaW5pbmcoaG91c2Vfc3BsaXQpCmhvdXNlX3Rlc3RpbmcgPC0gdGVzdGluZyhob3VzZV9zcGxpdCkKCiNSZWNyZWF0ZSB0aGUgcmFuZG9tIGZvcmVzdCBtb2RlbDoKCiMgc2V0IHVwIHJlY2lwZSBhbmQgdHJhbnNmb3JtYXRpb24gc3RlcHMgYW5kIHJvbGVzCnJhbmdlcl9yZWNpcGUgPC0gCiAgcmVjaXBlKGZvcm11bGEgPSBsb2dfcHJpY2UgfiAuLCAKICAgICAgICAgZGF0YSA9IGhvdXNlX3RyYWluaW5nKSAlPiUgCiAgc3RlcF9kYXRlKGRhdGUsIAogICAgICAgICAgICBmZWF0dXJlcyA9ICJtb250aCIpICU+JSAKICAjIE1ha2UgdGhlc2UgZXZhbHVhdGl2ZSB2YXJpYWJsZXMsIG5vdCBpbmNsdWRlZCBpbiBtb2RlbGluZwogIHVwZGF0ZV9yb2xlKGFsbF9vZihjKCJpZCIsCiAgICAgICAgICAgICAgICAgICAgICAgImRhdGUiKSksCiAgICAgICAgICAgICAgbmV3X3JvbGUgPSAiZXZhbHVhdGl2ZSIpCgojZGVmaW5lIG1vZGVsCnJhbmdlcl9zcGVjIDwtIAogIHJhbmRfZm9yZXN0KG10cnkgPSA2LCAKICAgICAgICAgICAgICBtaW5fbiA9IDEwLCAKICAgICAgICAgICAgICB0cmVlcyA9IDIwMCkgJT4lIAogIHNldF9tb2RlKCJyZWdyZXNzaW9uIikgJT4lIAogIHNldF9lbmdpbmUoInJhbmdlciIpCgojY3JlYXRlIHdvcmtmbG93CnJhbmdlcl93b3JrZmxvdyA8LSAKICB3b3JrZmxvdygpICU+JSAKICBhZGRfcmVjaXBlKHJhbmdlcl9yZWNpcGUpICU+JSAKICBhZGRfbW9kZWwocmFuZ2VyX3NwZWMpIAoKI2ZpdCB0aGUgbW9kZWwKc2V0LnNlZWQoNzEyKSAjIGZvciByZXByb2R1Y2liaWxpdHkgLSByYW5kb20gc2FtcGxpbmcgaW4gcmFuZG9tIGZvcmVzdCBjaG9vc2luZyBudW1iZXIgb2YgdmFyaWFibGVzCnJhbmdlcl9maXQgPC0gcmFuZ2VyX3dvcmtmbG93ICU+JSAKICBmaXQoaG91c2VfdHJhaW5pbmcpCgpgYGAKCgoKMS4gQ2hvb3NlIDMgbmV3IG9ic2VydmF0aW9ucyBhbmQgZG8gdGhlIGZvbGxvd2luZyBmb3IgZWFjaCBvYnNlcnZhdGlvbjogIAoKPGJyPiAKCiMjIyMjIFRoZSB0aHJlZSBvYnNlcnZhdGlvbnM6CgoKYGBge3J9CiMgQ3JlYXRlIGFuIGV4cGxhaW5lciBmb3IgdGhlIHJhbmRvbSBmb3Jlc3QgbW9kZWw6CnJmX2V4cGxhaW4gPC0gCiAgZXhwbGFpbl90aWR5bW9kZWxzKAogICAgbW9kZWwgPSByYW5nZXJfZml0LAogICAgZGF0YSA9IGhvdXNlX3RyYWluaW5nICU+JSBzZWxlY3QoLWxvZ19wcmljZSksIAogICAgeSA9IGhvdXNlX3RyYWluaW5nICU+JSAgcHVsbChsb2dfcHJpY2UpLAogICAgbGFiZWwgPSAicmYiCiAgKQpgYGAKCgpgYGB7cn0Kb2JzMSA8LSBob3VzZV90ZXN0aW5nICU+JSBzbGljZSg1Mzc3KSAKb2JzMiA8LSBob3VzZV90ZXN0aW5nICU+JSBzbGljZSgxMDApIApvYnMzIDwtIGhvdXNlX3Rlc3RpbmcgJT4lIHNsaWNlKDI0OTgpIAoKb2JzMQpvYnMyCm9iczMKCiMgUHJpY2Ugb2YgbmV3X29icydzIGhvdXNlIC0ganVzdCB0byBrbm93IGJlY2F1c2UgSSBjYW4ndCB0aGluayBpbiBsb2dzCjEwXihvYnMxJGxvZ19wcmljZSkKIyBvYnNlcnZhdGlvbiAxIGhvdXNlIHByaWNlIGlzICQxODYsNSwwMDAKMTBeKG9iczIkbG9nX3ByaWNlKQojIG9ic2VydmF0aW9uIDIgaG91c2UgcHJpY2UgaXMgJDM2NSwwMDAKMTBeKG9iczMkbG9nX3ByaWNlKQojIG9ic2VydmF0aW9uIDMgaG91c2UgcHJpY2UgaXMgJDUwNSwwMDAKYGBgCjxicj4KCiMjIyBQYXJ0IDEKCjxicj4KICAKICAKYGBge3J9CiNvYnMxIHJmCmJkX3JmX29iczEgPC0gcHJlZGljdF9wYXJ0cyhleHBsYWluZXIgPSByZl9leHBsYWluLAogICAgICAgICAgICAgICAgICAgICAgICAgIG5ld19vYnNlcnZhdGlvbiA9IG9iczEsCiAgICAgICAgICAgICAgICAgICAgICAgICAgdHlwZSA9ICJicmVha19kb3duIikgI2RlZmF1bHQKcGxvdChiZF9yZl9vYnMxKQpiZF9yZl9vYnMxCmBgYAoKYGBge3J9CiNvYnMyIHJmCmJkX3JmX29iczIgPC0gcHJlZGljdF9wYXJ0cyhleHBsYWluZXIgPSByZl9leHBsYWluLAogICAgICAgICAgICAgICAgICAgICAgICAgIG5ld19vYnNlcnZhdGlvbiA9IG9iczIsCiAgICAgICAgICAgICAgICAgICAgICAgICAgdHlwZSA9ICJicmVha19kb3duIikgI2RlZmF1bHQKcGxvdChiZF9yZl9vYnMyKQpiZF9yZl9vYnMyCmBgYAoKYGBge3J9CiNvYnMzIHJmCmJkX3JmX29iczMgPC0gcHJlZGljdF9wYXJ0cyhleHBsYWluZXIgPSByZl9leHBsYWluLAogICAgICAgICAgICAgICAgICAgICAgICAgIG5ld19vYnNlcnZhdGlvbiA9IG9iczMsCiAgICAgICAgICAgICAgICAgICAgICAgICAgdHlwZSA9ICJicmVha19kb3duIikgI2RlZmF1bHQKcGxvdChiZF9yZl9vYnMzKQpiZF9yZl9vYnMzCmBgYAogIAo8YnI+IAoKIyMjIyMgT2JzZXJ2YXRpb24gMTogVGhlIGxhcmdlc3QgY29udHJpYnV0aW9uIGlzIGJ5IHNxZnRfbGl2aW5nID0gMzU3MAoKPGJyPiAKCiMjIyMjIE9ic2VydmF0aW9uIDI6IFRoZSBsYXJnZXN0IGNvbnRyaWJ1dGlvbiBpcyBieSBsYXQgPSA0Ny42MjgzCgo8YnI+IAoKIyMjIyMgT2JzZXJ2YXRpb24gMzogVGhlIGxhcmdlc3QgY29udHJpYnV0aW9uIGlzIGJ5IGxhdCA9IDQ3LjU3MjIKCjxicj4KICAKIyMjIFBhcnQgMgoKPGJyPiAKICAKYGBge3IgY2FjaGU9VFJVRX0KI29icyAxCnJmX3NoYXAxIDwtcHJlZGljdF9wYXJ0cyhleHBsYWluZXIgPSByZl9leHBsYWluLAogICAgICAgICAgICAgICAgICAgICAgICBuZXdfb2JzZXJ2YXRpb24gPSBvYnMxLAogICAgICAgICAgICAgICAgICAgICAgICB0eXBlID0gInNoYXAiLAogICAgICAgICAgICAgICAgICAgICAgICBCID0gMTAgI251bWJlciBvZiByZW9yZGVyaW5ncyAtIHN0YXJ0IHNtYWxsCikKCnBsb3QocmZfc2hhcDEpCgoKI29icyAyCnJmX3NoYXAyIDwtcHJlZGljdF9wYXJ0cyhleHBsYWluZXIgPSByZl9leHBsYWluLAogICAgICAgICAgICAgICAgICAgICAgICBuZXdfb2JzZXJ2YXRpb24gPSBvYnMyLAogICAgICAgICAgICAgICAgICAgICAgICB0eXBlID0gInNoYXAiLAogICAgICAgICAgICAgICAgICAgICAgICBCID0gMTAgI251bWJlciBvZiByZW9yZGVyaW5ncyAtIHN0YXJ0IHNtYWxsCikKCnBsb3QocmZfc2hhcDIpCgojb2JzIDMKcmZfc2hhcDMgPC1wcmVkaWN0X3BhcnRzKGV4cGxhaW5lciA9IHJmX2V4cGxhaW4sCiAgICAgICAgICAgICAgICAgICAgICAgIG5ld19vYnNlcnZhdGlvbiA9IG9iczMsCiAgICAgICAgICAgICAgICAgICAgICAgIHR5cGUgPSAic2hhcCIsCiAgICAgICAgICAgICAgICAgICAgICAgIEIgPSAxMCAjbnVtYmVyIG9mIHJlb3JkZXJpbmdzIC0gc3RhcnQgc21hbGwKKQoKcGxvdChyZl9zaGFwMykKYGBgCgo8YnI+IAoKIyMjIyMgSXQgZG9lcyB0ZWxsIGEgc3Rvcnkgc2ltaWxhciB0byB0aGUgYnJlYWstZG93biBwbG90cyBmcm9tIGFib3ZlLgoKPGJyPiAKCiMjIyBQYXJ0IDMgCiAgCmBgYHtyfQojIE5FRUQgdGhlc2UgdHdvIGxpbmVzIG9mIGNvZGUgYWx3YXlzIQojIFRoZXkgbWFrZSBzdXJlIG91ciBleHBsYWluZXIgaXMgZGVmaW5lZCBjb3JyZWN0bHkgdG8gdXNlIGluIHRoZSBuZXh0IHN0ZXAKCgojb2JzZXJ2YXRpb24gMQpzZXQuc2VlZCgyKQptb2RlbF90eXBlLmRhbGV4X2V4cGxhaW5lciA8LSBEQUxFWHRyYTo6bW9kZWxfdHlwZS5kYWxleF9leHBsYWluZXIKcHJlZGljdF9tb2RlbC5kYWxleF9leHBsYWluZXIgPC0gREFMRVh0cmE6OnByZWRpY3RfbW9kZWwuZGFsZXhfZXhwbGFpbmVyCgpsaW1lX3JmXzEgPC0gcHJlZGljdF9zdXJyb2dhdGUoZXhwbGFpbmVyID0gcmZfZXhwbGFpbiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBuZXdfb2JzZXJ2YXRpb24gPSBvYnMxICU+JQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2VsZWN0KC1sb2dfcHJpY2UpLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBuX2ZlYXR1cmVzID0gNywKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBuX3Blcm11dGF0aW9ucyA9IDEwMDAsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdHlwZSA9ICJsaW1lIikKCmxpbWVfcmZfMSAlPiUgCiAgc2VsZWN0KG1vZGVsX3IyLCBtb2RlbF9wcmVkaWN0aW9uLCBwcmVkaWN0aW9uKSAlPiUgCiAgZGlzdGluY3QoKQoKcGxvdChsaW1lX3JmXzEpICsKICBsYWJzKHggPSAiVmFyaWFibGUiKQoKI29ic2VydmF0aW9uIDIKc2V0LnNlZWQoMikKbW9kZWxfdHlwZS5kYWxleF9leHBsYWluZXIgPC0gREFMRVh0cmE6Om1vZGVsX3R5cGUuZGFsZXhfZXhwbGFpbmVyCnByZWRpY3RfbW9kZWwuZGFsZXhfZXhwbGFpbmVyIDwtIERBTEVYdHJhOjpwcmVkaWN0X21vZGVsLmRhbGV4X2V4cGxhaW5lcgoKbGltZV9yZl8yIDwtIHByZWRpY3Rfc3Vycm9nYXRlKGV4cGxhaW5lciA9IHJmX2V4cGxhaW4sCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbmV3X29ic2VydmF0aW9uID0gb2JzMiAlPiUKICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgIHNlbGVjdCgtbG9nX3ByaWNlKSwgCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbl9mZWF0dXJlcyA9IDcsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgbl9wZXJtdXRhdGlvbnMgPSAxMDAwLAogICAgICAgICAgICAgICAgICAgICAgICAgICAgIHR5cGUgPSAibGltZSIpCgpsaW1lX3JmXzIgJT4lIAogIHNlbGVjdChtb2RlbF9yMiwgbW9kZWxfcHJlZGljdGlvbiwgcHJlZGljdGlvbikgJT4lIAogIGRpc3RpbmN0KCkKCnBsb3QobGltZV9yZl8yKSArCiAgbGFicyh4ID0gIlZhcmlhYmxlIikKCgojb2JzZXJ2YXRpb24gMwpzZXQuc2VlZCgyKQptb2RlbF90eXBlLmRhbGV4X2V4cGxhaW5lciA8LSBEQUxFWHRyYTo6bW9kZWxfdHlwZS5kYWxleF9leHBsYWluZXIKcHJlZGljdF9tb2RlbC5kYWxleF9leHBsYWluZXIgPC0gREFMRVh0cmE6OnByZWRpY3RfbW9kZWwuZGFsZXhfZXhwbGFpbmVyCgpsaW1lX3JmXzMgPC0gcHJlZGljdF9zdXJyb2dhdGUoZXhwbGFpbmVyID0gcmZfZXhwbGFpbiwKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBuZXdfb2JzZXJ2YXRpb24gPSBvYnMzICU+JQogICAgICAgICAgICAgICAgICAgICAgICAgICAgICAgc2VsZWN0KC1sb2dfcHJpY2UpLCAKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBuX2ZlYXR1cmVzID0gNywKICAgICAgICAgICAgICAgICAgICAgICAgICAgICBuX3Blcm11dGF0aW9ucyA9IDEwMDAsCiAgICAgICAgICAgICAgICAgICAgICAgICAgICAgdHlwZSA9ICJsaW1lIikKCmxpbWVfcmZfMyAlPiUgCiAgc2VsZWN0KG1vZGVsX3IyLCBtb2RlbF9wcmVkaWN0aW9uLCBwcmVkaWN0aW9uKSAlPiUgCiAgZGlzdGluY3QoKQoKcGxvdChsaW1lX3JmXzMpICsKICBsYWJzKHggPSAiVmFyaWFibGUiKQpgYGAKPGJyPiAKCiMjIyMjIFRoZSBiYXJzIHNob3cgd2hpY2ggdmFyaWFibGVzIGFyZSBtb3N0IGltcG9ydGFudC4gVGhlIEV4cGxhbmF0aW9uIGZpdCBpcyBhbiBvdmVyYWxsIHBlcmZvcm1hbmNlIG1ldHJpYyBmb3IgdGhlIGxvY2FsIG1vZGVsIC0gbW9kZWxfcjIgZnJvbSB0aGUgb3V0cHV0IGFib3ZlLiBPYnNlcnZhdGlvbiAxIGhhcyB0aGUgaGlnaGVzdCBleHBsYW5hdGlvbiBmaXQgKDAuMzcpLCBzbyBpdCBpcyB0aGUgYmVzdCBtb2RlbC4gSXQgbWVhbnMgdGhhdCBpdCdzIHByZWRpY3Rpb24gZnJvbSB0aGlzIGxvY2FsIG1vZGVsIGlzIGNsb3Nlc3QgdG8gaXRzIHByZWRpY3Rpb24gZnJvbSB0aGUgb3JpZ2luYWwgbW9kZWwuIAogIAo8YnI+IAoKIyMjIFBhcnQgMwoKIyMjIyMgSSB0aGluayB0aGUgdG9vbHMgd2UndmUgbGVhcm5lZCB3b3VsZCBiZSB2ZXJ5IGhlbHBmdWwgZm9yIHByZWRpY3RpdmUgbW9kZWxzLCBlc3BlY2lhbGx5IHdoZW4gdGhlcmUgbWF5IGJlIGxvdHMgb2YgdmFyaWFibGVzIGFuZCBpdCdsbCBoZWxwIHVzIG5hcnJvdyBkb3duIHRoZSBiZXN0IHZhcmlhYmxlcyB0byBtYWtlIHRoZSBiZXN0IHByZWRpY3Rpb25zLiAgCgoKIyMgU1FMCgpZb3Ugd2lsbCB1c2UgdGhlIGBhaXJsaW5lc2AgZGF0YSBmcm9tIHRoZSBTUUwgZGF0YWJhc2UgdGhhdCBJIHVzZWQgaW4gdGhlIGV4YW1wbGUgaW4gdGhlIFt0dXRvcmlhbF0oaHR0cHM6Ly9hZHZhbmNlZC1kcy1pbi1yLm5ldGxpZnkuYXBwL3Bvc3RzLzIwMjEtMDMtMjktc3FsaW5yLykuIEJlIHN1cmUgdG8gaW5jbHVkZSB0aGUgY2h1bmsgdG8gY29ubmVjdCB0byB0aGUgZGF0YWJhc2UgaGVyZS4gQW5kLCB3aGVuIHlvdSBhcmUgZmluaXNoZWQsIGRpc2Nvbm5lY3QuIFlvdSBtYXkgbmVlZCB0byByZWNvbm5lY3QgdGhyb3VnaG91dCBhcyBpdCB0aW1lcyBvdXQgYWZ0ZXIgYSB3aGlsZS4KCioqVGFza3MqKjoKCiMjIyBQYXJ0IDEKCmBgYHtyfQpjb25fYWlyIDwtIGRiQ29ubmVjdChSTXlTUUw6Ok15U1FMKCksIAogICAgICAgICAgICAgICAgICAgICBkYm5hbWUgPSAiYWlybGluZXMiLCAKICAgICAgICAgICAgICAgICAgICAgaG9zdCA9ICJtZHNyLmNkYzd0Z2trcWQwbi51cy1lYXN0LTEucmRzLmFtYXpvbmF3cy5jb20iLCAKICAgICAgICAgICAgICAgICAgICAgdXNlciA9ICJtZHNyX3B1YmxpYyIsIAogICAgICAgICAgICAgICAgICAgICBwYXNzd29yZCA9ICJJbWhzbWZsTURTd1IiKQoKZGJMaXN0VGFibGVzKGNvbl9haXIpCmBgYAoKYGBge3J9CiNsYXBwbHkoZGJMaXN0Q29ubmVjdGlvbnMoTXlTUUwoKSksIGRiRGlzY29ubmVjdCkKYGBgCgo8YnI+CgpgYGB7cn0KZmxpZ2h0cyA8LQogIHRibChjb25fYWlyLCAiZmxpZ2h0cyIpICU+JQogIHNlbGVjdChtb250aCwgYXJyX2RlbGF5LCBvcmlnaW4sIGRlc3QsIGFpcl90aW1lLCBkaXN0YW5jZSkgJT4lCiAgaGVhZCgxMDApCgojYnkgYWlycG9ydApmbGlnaHRzX2FpciA8LSAKICBmbGlnaHRzICU+JQogIGdyb3VwX2J5KG9yaWdpbikgJT4lCiAgc3VtbWFyaXplKG5fZmxpZ2h0cyA9IG4oKSwKICBhdmdfbGVuZ3RoID0gbWVhbihhaXJfdGltZSksCiAgYXZnX2Rpc3RhbmNlID0gbWVhbihkaXN0YW5jZSkpICU+JSAKICBpbm5lcl9qb2luKHRibChjb25fYWlyLCAiYWlycG9ydHMiKSwgCiAgICAgICAgICAgICAgYnkgPSBjKCJvcmlnaW4iID0gImZhYSIpKQphaXIgPC0gCiAgdGJsKGNvbl9haXIsICJmbGlnaHRzIikgJT4lIAogIGhlYWQoMTAwKSAlPiUgCiAgZ3JvdXBfYnkob3JpZ2luKSAlPiUgCiAgc3VtbWFyaXplKHByb3BfbGF0ZV9vdmVyMjAgPSBtZWFuKGFycl9kZWxheSA+IDIwKSkgJT4lIAogIGFycmFuZ2UoZGVzYyhwcm9wX2xhdGVfb3ZlcjIwKSkgJT4lCiAgcmVuYW1lKAogICAgIHByb3BfbGF0ZV9haXJwb3J0ID0gcHJvcF9sYXRlX292ZXIyMCkgIyU+JQogICAjYXMuZGF0YS5mcmFtZShhaXIsIHJvdy5uYW1lcyA9IFRSVUUpCgphaXJwb3J0X2luZm8gPC0KICBmbGlnaHRzX2FpciAlPiUKICBpbm5lcl9qb2luKGFpciwKICAgICAgICAgICAgYnkgPSBjKCJvcmlnaW4iID0gIm9yaWdpbiIpKSAlPiUKICBzZWxlY3QobmFtZSwgbl9mbGlnaHRzLCBhdmdfZGlzdGFuY2UsIGF2Z19sZW5ndGgsIHByb3BfbGF0ZV9haXJwb3J0KSAlPiUKICBhcnJhbmdlKGRlc2MocHJvcF9sYXRlX2FpcnBvcnQpKQogICNhcy5kYXRhLmZyYW1lKGZsaWdodHNfYWlyLCByb3cubmFtZXMgPSBUUlVFKQoKI2J5IG1vbnRoCmZsaWdodHNfbW9uIDwtIAogIGZsaWdodHMgJT4lCiAgZ3JvdXBfYnkobW9udGgpICU+JQogIHN1bW1hcml6ZShuX2ZsaWdodHMgPSBuKCksCiAgYXZnX2xlbmd0aCA9IG1lYW4oYWlyX3RpbWUpLAogIGF2Z19kaXN0YW5jZSA9IG1lYW4oZGlzdGFuY2UpKSAKCm1vbiA8LSAKICB0YmwoY29uX2FpciwgImZsaWdodHMiKSAlPiUgCiAgaGVhZCgxMDApICU+JSAKICBncm91cF9ieShtb250aCkgJT4lIAogIHN1bW1hcml6ZShwcm9wX2xhdGVfb3ZlcjIwID0gbWVhbihhcnJfZGVsYXkgPiAyMCkpICU+JSAKICBhcnJhbmdlKGRlc2MocHJvcF9sYXRlX292ZXIyMCkpICU+JQogICNhcy5kYXRhLmZyYW1lKG1vbiwgcm93Lm5hbWVzID0gVFJVRSkgJT4lCiAgcmVuYW1lKAogICAgcHJvcF9sYXRlX21vbnRoID0gcHJvcF9sYXRlX292ZXIyMCkKCm1vbnRoX2luZm8gPC0KZmxpZ2h0c19tb24gJT4lCiAgaW5uZXJfam9pbihtb24sCiAgICAgICAgICAgIGJ5ID0gYygibW9udGgiID0gIm1vbnRoIikpICU+JQogIHNlbGVjdChtb250aCwgbl9mbGlnaHRzLCBhdmdfZGlzdGFuY2UsIGF2Z19sZW5ndGgsIHByb3BfbGF0ZV9tb250aCkgJT4lCiAgYXJyYW5nZShkZXNjKHByb3BfbGF0ZV9tb250aCkpCiAgI2FzLmRhdGEuZnJhbWUoZmxpZ2h0c19tb24sIHJvdy5uYW1lcyA9IFRSVUUpCmBgYAoKCiMjIyMjIFNRTCBwb3J0aW9uCgpgYGB7cn0KY29uX2FpciA8LSBkYkNvbm5lY3QoUk15U1FMOjpNeVNRTCgpLCAKICAgICAgICAgICAgICAgICAgICAgZGJuYW1lID0gImFpcmxpbmVzIiwgCiAgICAgICAgICAgICAgICAgICAgIGhvc3QgPSAibWRzci5jZGM3dGdra3FkMG4udXMtZWFzdC0xLnJkcy5hbWF6b25hd3MuY29tIiwgCiAgICAgICAgICAgICAgICAgICAgIHVzZXIgPSAibWRzcl9wdWJsaWMiLCAKICAgICAgICAgICAgICAgICAgICAgcGFzc3dvcmQgPSAiSW1oc21mbE1EU3dSIikKCmRiTGlzdFRhYmxlcyhjb25fYWlyKQpgYGAKCmBgYHtyfQphaXJwb3J0X2luZm8gJT4lCiAgc2hvd19xdWVyeSgpCgptb250aF9pbmZvICU+JQogIHNob3dfcXVlcnkoKQpgYGAKCjxicj4KCiMjIyMjIGFpcnBvcnQgaW5mb3JtYXRpb24KYGBge3NxbCBjb25uZWN0aW9uPWNvbl9haXJ9ClNFTEVDVCBgbmFtZWAsIGBuX2ZsaWdodHNgLCBgYXZnX2Rpc3RhbmNlYCwgYGF2Z19sZW5ndGhgLCBgcHJvcF9sYXRlX2FpcnBvcnRgCkZST00gKFNFTEVDVCBgTEhTYC5gb3JpZ2luYCBBUyBgb3JpZ2luYCwgYExIU2AuYG5fZmxpZ2h0c2AgQVMgYG5fZmxpZ2h0c2AsIGBMSFNgLmBhdmdfbGVuZ3RoYCBBUyBgYXZnX2xlbmd0aGAsIGBMSFNgLmBhdmdfZGlzdGFuY2VgIEFTIGBhdmdfZGlzdGFuY2VgLCBgTEhTYC5gbmFtZWAgQVMgYG5hbWVgLCBgTEhTYC5gbGF0YCBBUyBgbGF0YCwgYExIU2AuYGxvbmAgQVMgYGxvbmAsIGBMSFNgLmBhbHRgIEFTIGBhbHRgLCBgTEhTYC5gdHpgIEFTIGB0emAsIGBMSFNgLmBkc3RgIEFTIGBkc3RgLCBgTEhTYC5gY2l0eWAgQVMgYGNpdHlgLCBgTEhTYC5gY291bnRyeWAgQVMgYGNvdW50cnlgLCBgUkhTYC5gcHJvcF9sYXRlX2FpcnBvcnRgIEFTIGBwcm9wX2xhdGVfYWlycG9ydGAKRlJPTSAoU0VMRUNUIGBMSFNgLmBvcmlnaW5gIEFTIGBvcmlnaW5gLCBgTEhTYC5gbl9mbGlnaHRzYCBBUyBgbl9mbGlnaHRzYCwgYExIU2AuYGF2Z19sZW5ndGhgIEFTIGBhdmdfbGVuZ3RoYCwgYExIU2AuYGF2Z19kaXN0YW5jZWAgQVMgYGF2Z19kaXN0YW5jZWAsIGBSSFNgLmBuYW1lYCBBUyBgbmFtZWAsIGBSSFNgLmBsYXRgIEFTIGBsYXRgLCBgUkhTYC5gbG9uYCBBUyBgbG9uYCwgYFJIU2AuYGFsdGAgQVMgYGFsdGAsIGBSSFNgLmB0emAgQVMgYHR6YCwgYFJIU2AuYGRzdGAgQVMgYGRzdGAsIGBSSFNgLmBjaXR5YCBBUyBgY2l0eWAsIGBSSFNgLmBjb3VudHJ5YCBBUyBgY291bnRyeWAKRlJPTSAoU0VMRUNUIGBvcmlnaW5gLCBDT1VOVCgqKSBBUyBgbl9mbGlnaHRzYCwgQVZHKGBhaXJfdGltZWApIEFTIGBhdmdfbGVuZ3RoYCwgQVZHKGBkaXN0YW5jZWApIEFTIGBhdmdfZGlzdGFuY2VgCkZST00gKFNFTEVDVCAqCkZST00gKFNFTEVDVCBgbW9udGhgLCBgYXJyX2RlbGF5YCwgYG9yaWdpbmAsIGBkZXN0YCwgYGFpcl90aW1lYCwgYGRpc3RhbmNlYApGUk9NIGBmbGlnaHRzYCkgYGRicGx5cl8yOTBgCkxJTUlUIDEwMCkgYGRicGx5cl8yOTFgCkdST1VQIEJZIGBvcmlnaW5gKSBgTEhTYApJTk5FUiBKT0lOIGBhaXJwb3J0c2AgQVMgYFJIU2AKT04gKGBMSFNgLmBvcmlnaW5gID0gYFJIU2AuYGZhYWApCikgYExIU2AKSU5ORVIgSk9JTiAoU0VMRUNUIGBvcmlnaW5gLCBgcHJvcF9sYXRlX292ZXIyMGAgQVMgYHByb3BfbGF0ZV9haXJwb3J0YApGUk9NIChTRUxFQ1QgKgpGUk9NIChTRUxFQ1QgYG9yaWdpbmAsIEFWRyhgYXJyX2RlbGF5YCA+IDIwLjApIEFTIGBwcm9wX2xhdGVfb3ZlcjIwYApGUk9NIChTRUxFQ1QgKgpGUk9NIGBmbGlnaHRzYApMSU1JVCAxMDApIGBkYnBseXJfMjkyYApHUk9VUCBCWSBgb3JpZ2luYCkgYGRicGx5cl8yOTNgCk9SREVSIEJZIGBwcm9wX2xhdGVfb3ZlcjIwYCBERVNDKSBgZGJwbHlyXzI5NGApIGBSSFNgCk9OIChgTEhTYC5gb3JpZ2luYCA9IGBSSFNgLmBvcmlnaW5gKQopIGBkYnBseXJfMjk1YApPUkRFUiBCWSBgcHJvcF9sYXRlX2FpcnBvcnRgIERFU0MKYGBgCjxicj4KCiMjIyMjIG1vbnRobHkgaW5mb3JtYXRpb24KYGBge3NxbCBjb25uZWN0aW9uPWNvbl9haXJ9ClNFTEVDVCBgbW9udGhgLCBgbl9mbGlnaHRzYCwgYGF2Z19kaXN0YW5jZWAsIGBhdmdfbGVuZ3RoYCwgYHByb3BfbGF0ZV9tb250aGAKRlJPTSAoU0VMRUNUIGBMSFNgLmBtb250aGAgQVMgYG1vbnRoYCwgYExIU2AuYG5fZmxpZ2h0c2AgQVMgYG5fZmxpZ2h0c2AsIGBMSFNgLmBhdmdfbGVuZ3RoYCBBUyBgYXZnX2xlbmd0aGAsIGBMSFNgLmBhdmdfZGlzdGFuY2VgIEFTIGBhdmdfZGlzdGFuY2VgLCBgUkhTYC5gcHJvcF9sYXRlX21vbnRoYCBBUyBgcHJvcF9sYXRlX21vbnRoYApGUk9NIChTRUxFQ1QgYG1vbnRoYCwgQ09VTlQoKikgQVMgYG5fZmxpZ2h0c2AsIEFWRyhgYWlyX3RpbWVgKSBBUyBgYXZnX2xlbmd0aGAsIEFWRyhgZGlzdGFuY2VgKSBBUyBgYXZnX2Rpc3RhbmNlYApGUk9NIChTRUxFQ1QgKgpGUk9NIChTRUxFQ1QgYG1vbnRoYCwgYGFycl9kZWxheWAsIGBvcmlnaW5gLCBgZGVzdGAsIGBhaXJfdGltZWAsIGBkaXN0YW5jZWAKRlJPTSBgZmxpZ2h0c2ApIGBkYnBseXJfMjk2YApMSU1JVCAxMDApIGBkYnBseXJfMjk3YApHUk9VUCBCWSBgbW9udGhgKSBgTEhTYApJTk5FUiBKT0lOIChTRUxFQ1QgYG1vbnRoYCwgYHByb3BfbGF0ZV9vdmVyMjBgIEFTIGBwcm9wX2xhdGVfbW9udGhgCkZST00gKFNFTEVDVCAqCkZST00gKFNFTEVDVCBgbW9udGhgLCBBVkcoYGFycl9kZWxheWAgPiAyMC4wKSBBUyBgcHJvcF9sYXRlX292ZXIyMGAKRlJPTSAoU0VMRUNUICoKRlJPTSBgZmxpZ2h0c2AKTElNSVQgMTAwKSBgZGJwbHlyXzI5OGAKR1JPVVAgQlkgYG1vbnRoYCkgYGRicGx5cl8yOTlgCk9SREVSIEJZIGBwcm9wX2xhdGVfb3ZlcjIwYCBERVNDKSBgZGJwbHlyXzMwMGApIGBSSFNgCk9OIChgTEhTYC5gbW9udGhgID0gYFJIU2AuYG1vbnRoYCkKKSBgZGJwbHlyXzMwMWAKT1JERVIgQlkgYHByb3BfbGF0ZV9tb250aGAgREVTQwpgYGAKCiAKYGBge3J9CmFpcnBvcnRfZGYgPC0gYXMuZGF0YS5mcmFtZShhaXJwb3J0X2luZm8sIHJvdy5uYW1lcyA9IFRSVUUpCm1vbnRoX2RmIDwtIGFzLmRhdGEuZnJhbWUobW9udGhfaW5mbywgcm93Lm5hbWVzID0gVFJVRSkKCndvcnN0XzEwX2FpcnBvcnRzIDwtCiAgYWlycG9ydF9kZiAlPiUKICBzZWxlY3QobmFtZSwgbl9mbGlnaHRzLCBhdmdfZGlzdGFuY2UsIGF2Z19sZW5ndGgsIHByb3BfbGF0ZV9haXJwb3J0KSAlPiUKICBhcnJhbmdlKGRlc2MocHJvcF9sYXRlX2FpcnBvcnQpKSAlPiUKICBoZWFkKDEwKQoKCmdncGxvdCh3b3JzdF8xMF9haXJwb3J0cywgYWVzKHggPSBuYW1lLCB5ID0gcHJvcF9sYXRlX2FpcnBvcnQpKSArIAogIGdlb21fY29sKCkgKwogIHRoZW1lKGF4aXMudGV4dC54ID0gZWxlbWVudF90ZXh0KGFuZ2xlID0gNjAsIGhqdXN0ID0gMSkpICsKICBsYWJzKHRpdGxlID0gIjEwIFdvcnN0IEFpcnBvcnRzIiwgeCA9ICJBaXJwb3J0IiwgeSA9ICJQcm9wb3J0aW9uIG9mID4yMCBtaW5zIGxhdGUiKQpgYGAKCiAgCmBgYHtyfQprbml0cjo6a2FibGUoaGVhZChhaXJwb3J0X2RmWzE6NiwgYygxLDUpXSksICJzaW1wbGUiKQpgYGAKICAKICAKIyMjIFBhcnQgMiAKCiMjIyMjIFF1ZXN0aW9uOiBXaGljaCBpcyB0aGUgbW9zdCBwb3B1bGFyIGFpcnBvcnQvIGFpcnBvcnQgd2l0aCBtb3N0IGZsaWdodHMKCmBgYHtyfQpnZ3Bsb3QoYWlycG9ydF9kZiwgYWVzKHggPSBuYW1lLCB5ID0gbl9mbGlnaHRzKSkgKyAKICBnZW9tX2NvbCgpICsKICB0aGVtZShheGlzLnRleHQueCA9IGVsZW1lbnRfdGV4dChhbmdsZSA9IDYwLCBoanVzdCA9IDEpKSArCiAgbGFicyh0aXRsZSA9ICJNb3N0IFBvcHVsYXIgQWlycG9ydCIsIHggPSAiQWlycG9ydCIsIHkgPSAiTnVtYmVyIG9mIERlcGFydHVyZXMiKQpgYGAKCiMjIEZ1bmN0aW9uIEZyaWRheQoKSWYgeW91IG5lZWQgdG8gcmV2aXNpdCB0aGUgbWF0ZXJpYWwsIGl0IGlzIHBvc3RlZCBvbiB0aGUgbW9vZGxlIHBhZ2UuIEkndmUgdHJpZWQgdG8gYWRkIGFsbCB0aGUgbmVjZXNzYXJ5IGxpYnJhcmllcyB0byB0aGUgdG9wLCBidXQgSSBtYXkgaGF2ZSBtaXNzZWQgc29tZXRoaW5nLgoKKipgZ2VvbV9zZigpYCB0YXNrcyoqOgoKIyMjIFBhcnQgMQoKYGBge3J9CmxpYnJhcnkoIm1hcHMiKQpsaWJyYXJ5KCJsd2dlb20iKQoKc3RhdGVzIDwtIHN0X2FzX3NmKG1hcCgic3RhdGUiLCBwbG90ID0gRkFMU0UsIGZpbGwgPSBUUlVFKSkKaGVhZChzdGF0ZXMpCgpzdGF0ZXMgPC0gc3RhdGVzICU+JQogIG11dGF0ZShhcmVhID0gYXMubnVtZXJpYyhzdF9hcmVhKHN0YXRlcykpKQoKCmdncGxvdChkYXRhID0gc3RhdGVzKSArCiAgICBnZW9tX3NmKGFlcyhmaWxsID0gYXJlYSkpICsKICAgIHNjYWxlX2ZpbGxfdmlyaWRpc19jKHRyYW5zID0gInNxcnQiLCBhbHBoYSA9IC40KSArCiAgICBjb29yZF9zZih4bGltID0gYygtMTI3LCAtNjMpLCAKeWxpbSA9IGMoMjQsIDUxKSwgCmV4cGFuZCA9IEZBTFNFKQpgYGAKCjxicj4gCgoKIyMjIFBhcnQgMgoKYGBge3J9CnN0YXRlcyA8LSBjYmluZChzdGF0ZXMsIHN0X2Nvb3JkaW5hdGVzKHN0X2NlbnRyb2lkKHN0YXRlcykpKQoKZ2dwbG90KGRhdGEgPSBzdGF0ZXMpICsKICAgIGdlb21fc2YoYWVzKGZpbGwgPSBhcmVhKSkgKwogICAgc2NhbGVfZmlsbF92aXJpZGlzX2ModHJhbnMgPSAic3FydCIsIGFscGhhID0gLjQpICsKICAgIGdlb21fcG9pbnQoZGF0YSA9IHN0YXRlcywgYWVzKFgsIFkpLCBzaXplID0gMSkgKwogICAgY29vcmRfc2YoeGxpbSA9IGMoLTEyNywgLTYzKSwgeWxpbSA9IGMoMjQsIDUxKSwgCmV4cGFuZCA9IEZBTFNFKQpgYGAKCgo8YnI+IAoKIyMjIFBhcnQgMyAKCgpgYGB7cn0KY291bnRpZXMgPC0gc3RfYXNfc2YobWFwKCJjb3VudHkiLCBwbG90ID0gRkFMU0UsIGZpbGwgPSBUUlVFKSkKY291bnRpZXMgPC0gc3Vic2V0KGNvdW50aWVzKQpjb3VudGllcyRhcmVhIDwtIGFzLm51bWVyaWMoc3RfYXJlYShjb3VudGllcykpCmhlYWQoY291bnRpZXMpCgoKZ2dwbG90KGRhdGEgPSBzdGF0ZXMpICsKICAgIGdlb21fc2YoYWVzKGZpbGwgPSBhcmVhKSkgKwogICAgZ2VvbV9zZihkYXRhID0gY291bnRpZXMsIGZpbGwgPSBOQSwgY29sb3IgPSBncmF5KC41KSkgKwogICAgc2NhbGVfZmlsbF92aXJpZGlzX2ModHJhbnMgPSAic3FydCIsIGFscGhhID0gLjQpICsKICAgIGdlb21fcG9pbnQoZGF0YSA9IHN0YXRlcywgYWVzKFgsIFkpLCBzaXplID0gMSkgKwogICAgY29vcmRfc2YoeGxpbSA9IGMoLTEyNywgLTYzKSwgeWxpbSA9IGMoMjQsIDUxKSwgCmV4cGFuZCA9IEZBTFNFKQpgYGAKPGJyPiAKCiMjIyBQYXJ0IDQgCgpgYGB7cn0KZ2dwbG90KGRhdGEgPSBzdGF0ZXMpICsKICAgIGdlb21fc2YoYWVzKGZpbGwgPSBhcmVhKSkgKwogICAgZ2VvbV9zZihkYXRhID0gY291bnRpZXMsIGZpbGwgPSBOQSwgY29sb3IgPSBncmF5KC41KSkgKwogICAgc2NhbGVfZmlsbF92aXJpZGlzX2ModHJhbnMgPSAic3FydCIsIGFscGhhID0gLjQpICsKICAgIGdlb21fdGV4dChkYXRhID0gc3RhdGVzLCBhZXMoWCwgWSwgbGFiZWwgPSBJRCksIHNpemUgPSA0KSArCiAgICBjb29yZF9zZih4bGltID0gYygtMTI1LCAtMTE0KSwgeWxpbSA9IGMoMzAsIDQyKSwgZXhwYW5kID0gRkFMU0UpCmBgYAoKCjxicj4KCgoqKmB0aWR5dGV4dGAgdGFza3MqKjoKCk5vdyB5b3Ugd2lsbCB0cnkgdXNpbmcgdGlkeXRleHQgb24gYSBuZXcgZGF0YXNldCBhYm91dCBSdXNzaWFuIFRyb2xsIHR3ZWV0cy4KCiMjIyMgUmVhZCBhYm91dCB0aGUgZGF0YQoKVGhlc2UgYXJlIHR3ZWV0cyBmcm9tIFR3aXR0ZXIgaGFuZGxlcyB0aGF0IGFyZSBjb25uZWN0ZWQgdG8gdGhlIEludGVybmV0IFJlc2VhcmNoIEFnZW5jeSAoSVJBKSwgYSBSdXNzaWFuICJ0cm9sbCBmYWN0b3J5LiIgIFRoZSBtYWpvcml0eSBvZiB0aGVzZSB0d2VldHMgd2VyZSBwb3N0ZWQgZnJvbSAyMDE1LTIwMTcsIGJ1dCB0aGUgZGF0YXNldHMgZW5jb21wYXNzIHR3ZWV0cyBmcm9tIEZlYnJ1YXJ5IDIwMTIgdG8gTWF5IDIwMTguCgpUaHJlZSBvZiB0aGUgbWFpbiBjYXRlZ29yaWVzIG9mIHRyb2xsIHR3ZWV0IHRoYXQgd2Ugd2lsbCBiZSBmb2N1c2luZyBvbiBhcmUgTGVmdCBUcm9sbHMsIFJpZ2h0IFRyb2xscywgYW5kIE5ld3MgRmVlZC4gICoqTGVmdCBUcm9sbHMqKiB1c3VhbGx5IHByZXRlbmQgdG8gYmUgQkxNIGFjdGl2aXN0cywgYWltaW5nIHRvIGRpdmlkZSB0aGUgZGVtb2NyYXRpYyBwYXJ0eSAoaW4gdGhpcyBjb250ZXh0LCBiZWluZyBwcm8tQmVybmllIHNvIHRoYXQgdm90ZXMgYXJlIHRha2VuIGF3YXkgZnJvbSBIaWxsYXJ5KS4gICoqUmlnaHQgdHJvbGxzKiogaW1pdGF0ZSBUcnVtcCBzdXBwb3J0ZXJzLCBhbmQgKipOZXdzIEZlZWQqKiBoYW5kbGVzIGFyZSAibG9jYWwgbmV3cyBhZ2dyZWdhdG9ycywiIHR5cGljYWxseSBsaW5raW5nIHRvIGxlZ2l0aW1hdGUgbmV3cy4KCkZvciBvdXIgdXBjb21pbmcgYW5hbHlzZXMsIHNvbWUgaW1wb3J0YW50IHZhcmlhYmxlcyBhcmU6CgogICogKiphdXRob3IqKiAoaGFuZGxlIHNlbmRpbmcgdGhlIHR3ZWV0KQogICogKipjb250ZW50KiogKHRleHQgb2YgdGhlIHR3ZWV0KQogICogKipsYW5ndWFnZSoqIChsYW5ndWFnZSBvZiB0aGUgdHdlZXQpCiAgKiAqKnB1Ymxpc2hfZGF0ZSoqIChkYXRlIGFuZCB0aW1lIHRoZSB0d2VldCB3YXMgc2VudCkKClZhcmlhYmxlIGRvY3VtZW50YXRpb24gY2FuIGJlIGZvdW5kIG9uIFtHaXRodWJdKGh0dHBzOi8vZ2l0aHViLmNvbS9maXZldGhpcnR5ZWlnaHQvcnVzc2lhbi10cm9sbC10d2VldHMvKSBhbmQgYSBtb3JlIGRldGFpbGVkIGRlc2NyaXB0aW9uIG9mIHRoZSBkYXRhc2V0IGNhbiBiZSBmb3VuZCBpbiB0aGlzIFtmaXZldGhpcnR5ZWlnaHQgYXJ0aWNsZV0oaHR0cHM6Ly9maXZldGhpcnR5ZWlnaHQuY29tL2ZlYXR1cmVzL3doeS13ZXJlLXNoYXJpbmctMy1taWxsaW9uLXJ1c3NpYW4tdHJvbGwtdHdlZXRzLykuCgpCZWNhdXNlIHRoZXJlIGFyZSAxMiBkYXRhc2V0cyBjb250YWluaW5nIDIsOTczLDM3MSB0d2VldHMgc2VudCBieSAyLDg0OCBUd2l0dGVyIGhhbmRsZXMgaW4gdG90YWwsIHdlIHdpbGwgYmUgdXNpbmcgdGhyZWUgb2YgdGhlc2UgZGF0YXNldHMgKG9uZSBmcm9tIGEgUmlnaHQgdHJvbGwsIG9uZSBmcm9tIGEgTGVmdCB0cm9sbCwgYW5kIG9uZSBmcm9tIGEgTmV3cyBGZWVkIGFjY291bnQpLgoKXApcCgojIyMjIyBQYXJ0IDEKCmBgYHtyLCBjYWNoZT1UUlVFfQp0cm9sbF90d2VldHMgPC0gcmVhZF9jc3YoImh0dHBzOi8vcmF3LmdpdGh1YnVzZXJjb250ZW50LmNvbS9maXZldGhpcnR5ZWlnaHQvcnVzc2lhbi10cm9sbC10d2VldHMvbWFzdGVyL0lSQWhhbmRsZV90d2VldHNfMTIuY3N2IikKYGBgCgo8YnI+IAoKIyMjIyMgUGFydCAyIAogIApgYGB7cn0KdHJvbGxfdHdlZXRzCnRyb2xsX3R3ZWV0cyA8LSAKICB0cm9sbF90d2VldHMgJT4lCiAgZmlsdGVyKGxhbmd1YWdlID09ICJFbmdsaXNoIikgCgpkaW0odHJvbGxfdHdlZXRzKQojRGltZW5zaW9ucyBhcmUgMTc1OTY2IGJ5IDIxCgoKbGlicmFyeShnZ3Bsb3QyKQoKZ2dwbG90KHRyb2xsX3R3ZWV0cywgYWVzKHggPSByZWdpb24pKSArIAogIGdlb21fYmFyKCkKCmdncGxvdCh0cm9sbF90d2VldHMsIGFlcyh4ID0gcmVnaW9uLCBmaWxsPWFjY291bnRfY2F0ZWdvcnkpKSArIAogIGdlb21fYmFyKCkrCiAgdGhlbWUoYXhpcy50ZXh0LnggPSBlbGVtZW50X3RleHQoYW5nbGUgPSA2MCwgaGp1c3QgPSAxKSkgKwoKZ2dwbG90KHRyb2xsX3R3ZWV0cywgYWVzKHggPSByZWdpb24sIGZpbGw9YWNjb3VudF90eXBlKSkgKyAKICBnZW9tX2JhcigpKwogIHRoZW1lKGF4aXMudGV4dC54ID0gZWxlbWVudF90ZXh0KGFuZ2xlID0gNjAsIGhqdXN0ID0gMSkpIApgYGAKCgojIyMjIyBQYXJ0IDMgCgpgYGB7cn0KdHJvbGxfdHdlZXRzX3VudG9rZW4gPC0gdHJvbGxfdHdlZXRzICU+JQogIHVubmVzdF90b2tlbnMod29yZCxjb250ZW50KQoKdHJvbGxfdHdlZXRzX3VudG9rZW4KYGBgCgpcClwKCiMjIyMjIFBhcnQgNAoKYGBge3J9CiNnZXQgcmlkIG9mIHN0b3B3b3JkcyAodGhlLCBhbmQsIGV0Yy4pCgp0cm9sbF90d2VldHNfY2xlYW5lZCA8LSB0cm9sbF90d2VldHNfdW50b2tlbiAlPiUKICBhbnRpX2pvaW4oZ2V0X3N0b3B3b3JkcygpKQpgYGAKCmBgYHtyfQp0cm9sbF90d2VldHNfY2xlYW5lZCA8LSB0cm9sbF90d2VldHNfY2xlYW5lZCAlPiUKICBmaWx0ZXIod29yZCAhPSAiaHR0cCIpICU+JSAKICBmaWx0ZXIod29yZCAhPSAiaHR0cHMiKSAlPiUKICBmaWx0ZXIod29yZCAhPSAidC5jbyIpICU+JSAKICBmaWx0ZXIod29yZCAhPSAicnQiKSAlPiUKICBmaWx0ZXIod29yZCAhPSAiYW1wIikgJT4lCiAgZmlsdGVyKHdvcmQgIT0gInQsY28iKSAlPiUKICBmaWx0ZXIod29yZCAhPSAiYW1wIikgJT4lCiAgZmlsdGVyKHdvcmQgIT0gKDE6OSkpIAoKICAKdHJvbGxfdHdlZXRzX2NsZWFuZWQKYGBgCgoKPGJyPiAKCiMjIyMjIFBhcnQgNQoKYGBge3IsIGV2YWw9VFJVRX0KdHJvbGxfdHdlZXRzX3NtYWxsIDwtIHRyb2xsX3R3ZWV0c19jbGVhbmVkICU+JQogIGNvdW50KHdvcmQpICU+JQogIHNsaWNlX21heChvcmRlcl9ieSA9IG4sIG4gPSA1MCkgIyA1MCBtb3N0IG9jY3VycmluZyB3b3JkcwoKdHJvbGxfdHdlZXRzX3NtYWxsCgojIHZpc3VhbGl6ZSB0aGUgbnVtYmVyIG9mIHRpbWVzIHRoZSA1MCB0b3Agd29yZHMgYXBwZWFyCmdncGxvdCh0cm9sbF90d2VldHNfc21hbGwsIAogICAgICAgYWVzKHggPSB3b3JkLCB5ID0gbikpICsKICBnZW9tX2NvbCgpICsKICB0aGVtZShheGlzLnRleHQueCA9IGVsZW1lbnRfdGV4dChhbmdsZSA9IDYwLCBoanVzdCA9IDEpKSAKYGBgCjxicj4KCiMjIyMjIFBhcnQgNgoKYGBge3J9CiMgbG9vayBhdCBzZW50aW1lbnQKc2VudGltZW50IDwtIGdldF9zZW50aW1lbnRzKCJiaW5nIikKc2VudGltZW50CgoKIyBhc3NpZ24gYSBzZW50aW1lbnQgdG8gZWFjaCB3b3JkIHRoYXQgaGFzIG9uZSBhc3NvY2lhdGVkCnRyb2xsX3R3ZWV0c19zZW50aW1lbnQgPC0gdHJvbGxfdHdlZXRzX2NsZWFuZWQgJT4lCiAgaW5uZXJfam9pbihzZW50aW1lbnQsCiAgICAgICAgICAgICBieSA9IGMoIndvcmQiID0gIndvcmQiKSkKCnRyb2xsX3R3ZWV0c19zZW50aW1lbnQKCiMgY291bnQgdGhlIHNlbnRpbWVudHMKdHJvbGxfdHdlZXRzX3NlbnRpbWVudCAlPiUgCiAgZ3JvdXBfYnkoc2VudGltZW50KSAlPiUKICBjb3VudCgpCmBgYAoKCjxicj4gCgojIyMjIyBUaGVyZSBhcmUgbW9yZSBuZWdhdGl2ZSB3b3JkcyB0aGFuIHBvc2l0aXZlIHdvcmRzLiBJIHRoaW5rIHRoaXMgaXMgYmVjYXVzZSB0aGVzZSBhcmUgdHJvbGwgdHdlZXRzIHRoZXkgYXJlIG1vcmUgbGlrZWx5IHRvIGJlIG5lZ2F0aXZlLiAKCjxicj4KCjcuIFVzaW5nIHRoZSB0cm9sbF90d2VldHNfc21hbGwgZGF0YXNldCwgbWFrZSBhIHdvcmRjbG91ZDoKCiAgYS4gVGhhdCBpcyBzaXplZCBieSB0aGUgbnVtYmVyIG9mIHRpbWVzIHRoYXQgYSB3b3JkIGFwcGVhcnMgaW4gdGhlIHR3ZWV0cwogIGIuIFRoYXQgaXMgY29sb3JlZCBieSBzZW50aW1lbnQgKHBvc2l0aXZlIG9yIG5lZ2F0aXZlKQoKCkJlIHN1cmUgdG8gcmVtb3ZlIHRoZSBgZXZhbD1GQUxTRWAhISEhCgpgYGB7cn0KIyBtYWtlIGEgd29yZGNsb3VkIHdoZXJlIHRoZSBzaXplIG9mIHRoZSB3b3JkIGlzIGJhc2VkIG9uIHRoZSBudW1iZXIgb2YgdGltZXMgdGhlIHdvcmQgYXBwZWFycyBhY3Jvc3MgdGhlIHR3ZWV0cwoKdHJvbGxfdHdlZXRzX3NtYWxsICU+JQogIHdpdGgod29yZGNsb3VkKHdvcmQsIG4sIG1heC53b3JkcyA9IDM1KSkKCnRyb2xsX3R3ZWV0c19zZW50aW1lbnQKCiMgbWFrZSBhIHdvcmRjbG91ZCBjb2xvcmVkIGJ5IHNlbnRpbWVudAp0cm9sbF90d2VldHNfc2VudGltZW50ICU+JQogIGFjYXN0KHdvcmQgfiBzZW50aW1lbnQpICU+JQogIGNvbXBhcmlzb24uY2xvdWQoY29sb3JzID0gYygicmVkIiwiYmx1ZSIpLAogICAgICAgICAgICAgICAgICAgbWF4LndvcmRzID0gMzUpCmBgYAoKPGJyPiAKCiMjIyMjIE5vLCB0aGV5J3JlIG5vdCByZWFsbHkgc3VycHJpc2luZy4gCgoKIyMgUHJvamVjdHMKCjxicj4KCiMjIyMjIEkgYW0gY3VycmVudGx5IGludGVyZXN0ZWQgaW4gbG9va2luZyBhdCByZW50YWwgYW5kIGhvdXNpbmcgZGF0YSBhbmQgdGhpbmsgaXQgd291bGQgYmUgY29vbCB0byBkbyBhIHByZWRpY3RpdmUgbW9kZWwgZm9yIGhvdXNpbmcsIGxvb2tpbmcgYXQgbmVpZ2hib3Job29kcywgc2Nob29sIGRpc3RyaWN0cywgcGFya3MsIGNyaW1lIGV0Yy4gSSB3aWxsIHNheSBpdCBmZWVscyBsaWtlIG91ciBncm91cCBkaWRuJ3QgcmVhbGx5IGNvbWUgdG8gYSBjb25jbHVzaW9uIG9uIHdoYXQgd2Ugd2FudCB0byB3b3JrIHdpdGgsIGJ1dCBJIHdvdWxkIGJlIGludGVyZXN0ZWQgaW4gZG9pbmcgYXBhcnRtZW50IHJlbnRhbHMgYW5kIGhvdXNpbmcgbWFya2V0LiBBbHNvLCB0aGUgc2hvb3RpbmcgZGF0YXNldCBhbHNvIGxvb2tzIHZlcnkgaW50ZXJlc3RpbmcuIAoKCiMjICJVbmRvaW5nIiBiaWFzCgoqKlRhc2s6KioKCjxicj4gCgojIyMjIyBGb3IgbWUsIHRoZSBtb3N0IGltcG9ydGFudCB0YWtlIGF3YXkgZnJvbSB0aGUgdGhyZWFkIHdhcyB0aGF0IGJpYXMgY2FuIGVtZXJnZSBkdXJpbmcgYW55IHBhcnQgb2YgdGhlIChNYWNoaW5lIGxlYXJuaW5nIHByb2Nlc3M/Pz8pLiBUaGlzIGluY2x1ZGVzIHNvbWUgb2YgdGhlIG1vcmUgb2J2aW91cyBzb3VyY2VzIGxpa2UgZGF0YSBwcm9jdXJlbWVudCB3aGVyZSB0aGUgc2FtcGxlIG1heSBiZSBiaWFzZWQuIEJ1dCBpdCBjYW4gYWxzbyBpbmNsdWRlIGEgc2VlbWluZ2x5IGJpYXMgZnJlZSBhbGdvcml0aG0uIEZvciBleGFtcGxlLCB0aGUgaW50cm9kdWN0aW9uIG9mIGEgbWFjaGluZSBsZWFybmluZyBhbGdvcml0aG0gdG8ganVkZ2VtZW50IGRlY2lzaW9ucyBmb3IgY3JpbWluYWwgZGVmZW5kYW50cyBhY3R1YWxseSBsZWQgdG8gYW4gaW5jcmVhc2UgaW4gcmFjaWFsIGRpc3Bhcml0aWVzLiBUaGlzIGlzIGJlY2F1c2UganVkZ2VzIG92ZXJyb2RlIHRoZSBhbGdvcml0aG1zIHNjb3JlcyBtb3JlIGluIHByZWRvbWluYXRlbHkgYmxhY2sgY29tbXVuaXRpZXMgY29tcGFyZWQgdG8gcHJlZG9taW5hdGVseSB3aGl0ZSBjb21tdW5pdGllcy4gQWRkaXRpb25hbGx5LCBqdWRnZXMgd2VyZSBtb3JlIGxpa2VseSB0byBvdmVydHVybiB0aGUgYWxnb3JpdGhtcyBkZWNpc2lvbiBmb3IgYSBoYXJzaGVyIHNlbnRlbmNlIGlmIHRoZSBzZW50ZW5jZSB3YXMgYmxhY2suCgo8YnI+Cg==